﻿/*
 * User: matsu
 * Date: 5/30/2010
 * Time: 3:00 PM
 * This code is from C# 3.0 in a Nutshell by Joseph and Ben Albahari,
 * 	adapted by Matthew MacGregor 2010.
 *
 * Copyright (c) 2010 Matthew MacGregor

 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:

 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.

 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 * 
 * 
 */

using System;
using System.IO;
using System.Net;
using System.Text;
using System.Threading;
using System.Windows.Forms;

//TODO: Improve handling of command line arguments


class WebServer
{
	private HttpListener _listener;
    private string _phpExePath;
    private string _phpExeName;
    private string _baseUrl;   // http://localhost:8080/
    private string _root;      // Your web page folder.
    
    public string BaseUrl {
        get { return _baseUrl; }
        set { _baseUrl = value; }
    }

    public string Root {
        get { return _root; }
        set { _root = value; }
    }
    
    
    public string PhpExePath {
        get { return _phpExePath; }
        set { _phpExePath = value; }
    }


    public string PhpExeName {
        get { return _phpExeName; }
        set { _phpExeName = value; }
    }
	
	public string Version {
		get { return System.Reflection.Assembly.
	            GetExecutingAssembly().GetName().Version.ToString(); }
	}

    public HttpListener Listener {
        get { return _listener; }
        set { _listener = value; }
    }

	public WebServer (string uriPrefix, string baseFolder)
	{

		System.Threading.ThreadPool.SetMaxThreads (50, 1000);
		System.Threading.ThreadPool.SetMinThreads (50, 50);
		_listener = new HttpListener();
		_listener.Prefixes.Add (uriPrefix);
		this.BaseUrl = uriPrefix;
		this.Root = baseFolder;
		this.PhpExeName = "php-cgi.exe";
		this.PhpExePath = "";
	}

	public void Start()  
	{                             // Run this on a separate thread, as
	                              // we did before.
	    try
		{
		    _listener.Start();    
    		while (true)
    		{
    
    			HttpListenerContext request = _listener.GetContext();
    			ThreadPool.QueueUserWorkItem (ProcessRequest, request);
    		}
		}
		catch ( HttpListenerException httple )  // Listener stopped.     
		{ 
		    Console.WriteLine( httple.Message );
		}   
		catch ( InvalidOperationException ioe )    // Listener stopped.
		{   
		    Console.WriteLine( ioe.Message );
		}
		
	}

	public void Stop() { _listener.Stop(); }
	

	void ProcessRequest (object listenerContext)
	{
	    
		try
		{
			var context = (HttpListenerContext) listenerContext;
			//string filename = Path.GetFileName( context.Request.RawUrl );
			string filename = context.Request.RawUrl;
			string path = Path.Combine ( this.Root, filename.TrimStart( '/' ));
			string[] defaults = { "index.html", "index.htm", "index.php" };

			byte[] msg;
			/*Empty filename means that we're trying to serve up a directory instead
			 * of a file.  In that case, look for a default file, then serve that. */
			if ( filename.EndsWith("/" ) )
			{
				path = findDefaultPage( path, defaults );
			}
			/**/
			if (!File.Exists (path))
			{
				context.Response.StatusCode = (int) HttpStatusCode.NotFound;
				msg = Encoding.UTF8.GetBytes ("Sorry, that page does not exist");
			}
			//If the script has a 'php' extension
			else if ( path.ToLower().EndsWith( ".php" ) )
			{
				//path to php file, and timeout in milliseconds
				string output = parsePHP( path, 30000 );
				context.Response.StatusCode = (int) HttpStatusCode.OK;
				msg = Encoding.UTF8.GetBytes( output );
			}
			else if ( path.ToLower().EndsWith( ".txt" ) )
			{
			    context.Response.StatusCode = (int) HttpStatusCode.OK;
			    context.Response.ContentType = "text/plain";
			    msg = File.ReadAllBytes( path );
			}
			else
			{
				context.Response.StatusCode = (int) HttpStatusCode.OK;
				
				msg = File.ReadAllBytes (path);
			}
			context.Response.ContentLength64 = msg.Length;
			using (Stream s = context.Response.OutputStream)
				s.Write (msg, 0, msg.Length);
		}
		catch (Exception ex) { Console.WriteLine ("Request error: " + ex); }
	}
	
	/*Look for the path to a default page, as defined by the filenames provided
	 * int the defaults array.  Return the best selection available.  (The file
	 * may not exist). */
	private string findDefaultPage( string path, string[] defaults )
	{
		string filename = "";
		//serve up a default file instead...
		foreach ( string d in defaults )
		{
			filename = d;
			if( File.Exists( Path.Combine( path, filename ) ) )
			{
				Console.WriteLine( "Found default:" + Path.Combine( path, filename ));
				//If we found a default, exit the loop.
				break;
			}
		}
		//If we didn't find a default file in the loop, then path will be
		//set to the last default filename checked.  This will force the
		//if ( !File.Exists ) check to be false, which will return an error
		//page.  This is preferred as a default.
		path = Path.Combine( path, filename );
		Console.WriteLine( path );
		return path;
	}
	
	//Creates a new process to parse a php file and returns the output.
	private string parsePHP( string path, int timeout  )
	{
		System.Diagnostics.Process proc = new System.Diagnostics.Process();
		proc.EnableRaisingEvents=false;
		proc.StartInfo.UseShellExecute=false;
		//Use the php-cgi executable
		proc.StartInfo.FileName=Path.Combine( this.PhpExePath, this.PhpExeName );
		//Parse the file, and do not include header information.
		//This program provides the headers.
		proc.StartInfo.Arguments="-f " + '"' + path + '"';
		//Standard output will be captured
		proc.StartInfo.RedirectStandardOutput=true;
		proc.StartInfo.RedirectStandardError=true;
		
		string output = "";
		string error  = "";
		//Start the process, then wait 30 seconds for completion
		try
		{
		    proc.Start();
		    //Capture output and error output.
		    output = proc.StandardOutput.ReadToEnd();
		    error = proc.StandardError.ReadToEnd();
		    output += error;
		    //Wait 30 seconds for completion.
		    proc.WaitForExit( timeout );
		    
		}
		catch ( Exception e )
		{
		    Console.WriteLine( "Exception Type: " + e.GetType().ToString() );
		    Console.WriteLine( e.Message );
		    this.Stop();
		}


		return output;
	}
	
	static void Main( string[] args)
	{
	    if (!HttpListener.IsSupported) 
            { 
                Console.WriteLine("Windows XP SP2 or Server 2003 is required to use the HttpListener class."); 
                return; 
            } 
		// Defaults are set in processArgs
		// Default port is 8080, default root directory is 
		//  the directory with the application exe in it.


		ApplicationArguments appArgs;
		
		if ( processArgs( args, out appArgs ) )
		{
		        var server = appArgs.GenerateWebServer();
    			// Start the server on a parallel thread:
    			new System.Threading.Thread (server.Start).Start();
    			string line = System.String.Format( 
    			              	"Cinch Server version {0} " +
    			              	"running on {1}\npress Enter to stop...",
    			              	server.Version, server.BaseUrl );
    			//TODO:  clean up processArgs, argument handling...
    			Console.WriteLine( line );
    			Console.WriteLine( "Serving pages from: " + server.Root );
    			//Console.ReadLine();
    			//try
    			//{
    			//    server.Stop();
    			//}
    			//catch ( ObjectDisposedException ) {} //server already stopped

		}
		else
		{
			Console.WriteLine("Invalid arguments.");
			Console.ReadLine();
			
		}
		
		
	}
	
	/*Accepts the array of arguments, and uses the out parameters to return 
	 * values of port, and root.  Command line args are:
	 * exename [port_number root_dir] */
	static bool processArgs( string[] args, out ApplicationArguments appArgs )
	{
	    appArgs = new ApplicationArguments();
		bool success = false;
		// Default port is 8080
		appArgs.Port = "http://localhost:8080/";
		// Default root directory is the one with the executable in it
		appArgs.Root =  Path.GetDirectoryName(Application.ExecutablePath);
		appArgs.PhpExeName = "php-cgi.exe";
		appArgs.PhpExePath = "";
		
			//too many args
			if ( args.Length > 3 )
			{
				success = false;
			}
			//No arguments provided, so go with the defaults.
			else if ( args.Length == 0 )
			{
				success = true;
			}
			else
			{
				if ( args.Length >= 1 )
				{
					string _port = args[0];

					int num;
					//Check to see if the argument is a number
					if( int.TryParse( _port, out num ) )
					{
						appArgs.Port = 	System.String.Format(
					                    "http://localhost:{0}/" , _port );
						success      = 	true;
					}
				}
			    //note: not 'else if'
				if ( args.Length >= 2 ) 
				{
					string _root = args[1];
					//Check to see if the root directory exists.
					if( Directory.Exists( _root ) || 
					    Directory.Exists( Path.Combine( appArgs.Root, _root ) ) )
					{
					   	//Path is absolute
					   	if ( Path.IsPathRooted( _root ) )
					   	{
					   		appArgs.Root = _root;
					   	}
					   	else
					   	//Path is relative
					   	{
					   		appArgs.Root = Path.Combine( appArgs.Root, _root );
					   	}
					   	success = true;
					}
					else
					{
					   	success = false;
					}
				
				}
				//note: not else if
				if( args.Length == 3 )
				{
				    try
				    {
				        string php_path = Path.GetDirectoryName( args[2] );
				        string php_exe = Path.GetFileName( args[2] );
				        appArgs.PhpExePath = php_path;
				        appArgs.PhpExeName = php_exe;
				        success = true;
				    }
				    catch ( Exception e )
				    {
				        Console.WriteLine( e.Message );
				        success = false;
				    }
				}
				
			}
		return success;
		
	}
	

	
}


class ApplicationArguments
{
    private string _port;
    
    public string Port {
        get { return _port; }
        set { _port = value; }
    }
    private string _root;
    
    public string Root {
        get { return _root; }
        set { _root = value; }
    }
    private string _phpExePath;
    
    public string PhpExePath {
        get { return _phpExePath; }
        set { _phpExePath = value; }
    }
    private string _phpExeName;
    
    public string PhpExeName {
        get { return _phpExeName; }
        set { _phpExeName = value; }
    }
    
    public WebServer GenerateWebServer()
    {
        var server = new WebServer ( this.Port, this.Root );
    	server.PhpExeName = this.PhpExeName;
    	server.PhpExePath = this.PhpExePath;
    	return server;
    }
    
}